package cn.trigram.file;

import cn.trigram.asserts.AssertUtil;
import cn.trigram.concurrent.ThreadPoolUtil;
import cn.trigram.exception.FileOperationException;
import cn.trigram.file.FileSplitSegmentNamePolicy.DefaultSplitFileNamePolicy;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;

/**
 * @author 一日看尽长安花
 */
public class FileUtil {

  private final static Pattern EXTENSION_PATTERN = Pattern
      .compile("([\\w\\s&&[^\\n\\r]]+)\\.([\\w\\s&&[^\\n\\r]]+)");

  public final static int KB = 1024;

  public final static int MB = 1048576;

  public final static int GB = 1073741824;

  public final static long TB = 1099511627776L;

  public final static long PB = 1125899906842624L;

  private final static ExecutorService splitFileExecutorService =
      new ThreadPoolExecutor(0, 10, 60L, TimeUnit.SECONDS,
                             new LinkedTransferQueue<Runnable>(),
                             ThreadPoolUtil.buildThreadFactory("seg-handle-file")
      );

  /**
   * 获取文件扩展名，如果没有则返回参数的原文件名
   */
  public static String getFileExtension(String fileName) {

    if (StringUtils.isBlank(fileName)) {
      return null;
    }
    Matcher matcher = EXTENSION_PATTERN.matcher(fileName);
    if (!matcher.find()) {
      return fileName;
    }
    return matcher.group(2);
  }

  /**
   * 获取不包括扩展的文件名，如果没有则返回参数的原文件名
   */
  public static String getFileNameNoExtension(String fileName) {

    if (StringUtils.isBlank(fileName)) {
      return null;
    }
    Matcher matcher = EXTENSION_PATTERN.matcher(fileName);
    if (!matcher.find()) {
      return fileName;
    }
    return matcher.group(1);
  }

  /**
   * 只是把不同的方案列一下，不可用
   */
  public static void writeBigDataScheme(final File outFile) throws IOException {

    String word2048 = "";
    /*第一个：BufferedWriter和直接使用FileOutputStream区别不大，但是字符流肯定更慢，所以没必要就直接使用字节流*/
    FileOutputStream fos    = new FileOutputStream(outFile);
    BufferedWriter   writer = new BufferedWriter(new OutputStreamWriter(fos));
    int              i      = 1000000;
    while (i > 0) {
      //  word2048为字符串常量，刚好4800个字节
      writer.write(word2048);
      i--;
    }
    writer.close();
    fos.close();
    /*第二个：使用ByteBuffer，比第一个快*/
    fos = new FileOutputStream(outFile);
    FileChannel fc = fos.getChannel();
//  此数字可优化
    int times = 100;
//  word2048为字符串常量，刚好4800个字节
    byte[]     datas = word2048.getBytes();
    ByteBuffer bbuf  = ByteBuffer.allocate(4800 * times);
    i = 10000;
    while (i > 0) {
      for (int j = 0; j < times; j++) {
        bbuf.put(datas);
      }
      bbuf.flip();
      fc.write(bbuf);
      bbuf.clear();
      i--;
    }
    /*第三个：使用直接内存映射。速度最快，但太耗内存了*/
    //  必须采用RandomAccessFile，并且是rw模式
    RandomAccessFile acf = new RandomAccessFile(outFile, "rw");
    fc = acf.getChannel();
    byte[] bs     = word2048.getBytes();
    int    len    = bs.length * 1000;
    long   offset = 0;
    i = 2000000;
    while (i > 0) {
      MappedByteBuffer mbuf = fc.map(FileChannel.MapMode.READ_WRITE, offset, len);
      for (int j = 0; j < 1000; j++) {
        mbuf.put(bs);
      }
      offset = offset + len;
      i      = i - 1000;
    }
    fc.close();
  }

  /**
   * 默认输出目录以 ["splits_" + 原文件名] 命名，在同个父级目录中。 每个分割文件的分段大小为50MB。 每个文件的名字以 [ "split_"+ 分段编号 + 原文件名] 命名。
   * 以系统默认行分隔符分隔。 处理分割时的缓存为1MB。查找分隔符的缓存为1KB
   *
   * @see #splitFile(File, File, long, FileSplitSegmentNamePolicy, byte[], int, int)
   */
  public static void splitFile(File sourceFile) throws IOException {

    AssertUtil.assertArgs(sourceFile.exists(), sourceFile.getName() + "不存在");
    //    创建splits+文件名的目录，存放分割后的文件
    String fileNameNoExtension = getFileNameNoExtension(sourceFile.getName());
    File outDir = sourceFile.toPath().getParent().resolve("splits_" + fileNameNoExtension)
                            .normalize().toFile();
    if (!outDir.exists()) {
      outDir.mkdirs();
    }
    splitFile(sourceFile, outDir, 50 * MB, new DefaultSplitFileNamePolicy(),
              System.lineSeparator().getBytes(), MB, KB
    );
  }

  /**
   * 单线程将大文件分割为小文件
   *
   * @param sourceFile
   *     原大文件
   * @param outDir
   *     分割文件存放的目录
   * @param splitSize
   *     每个分割文件的大小，大致的一个大小，如果直接按这个大小分割，有可能会破坏文件内容。内部会对文件精确分割
   * @param splitFileNamePolicy
   *     分割文件命名的策略
   * @param separatorBytes
   *     分割文件内容的分隔符号的字符串的字节数组
   * @param splitFileCapacityBufferSize
   *     分割文件时的ByteBuffer缓存大小
   * @param prereadSeparatorBufferSize
   *     以分隔符精确查找分割位置时的ByteBuffer的缓存大小，不建议太小，也不建议太大了，使用时估算一下2-3倍一个内容的大小。 不然也会有少许问题，比如，正好在分隔符的位置破坏了
   */
  public static void splitFile(
      File sourceFile, File outDir, long splitSize,
      FileSplitSegmentNamePolicy splitFileNamePolicy, byte[] separatorBytes,
      int splitFileCapacityBufferSize, int prereadSeparatorBufferSize
  ) throws IOException {

    ByteBuffer channelBuffer     = ByteBuffer.allocate(splitFileCapacityBufferSize);
    byte[]     channelBufferDest = new byte[splitFileCapacityBufferSize];
    new FileSplitSegmentHandle(sourceFile, separatorBytes, splitSize, prereadSeparatorBufferSize)
        .split((sourceFilePath, segmentNum, startPosition, endPosition) -> {

//      开始分割文件
          File splitFile = new File(
              outDir,
              splitFileNamePolicy.transformer(sourceFile.getName(), segmentNum)
          );
          try (
              RandomAccessFile randomAccessFile = new RandomAccessFile(sourceFilePath, "r");
              FileChannel channel = randomAccessFile.getChannel();
              FileOutputStream fos = new FileOutputStream(splitFile);
              BufferedOutputStream bos = new BufferedOutputStream(fos)
          ) {
//        调整到当前段的起始位置
            channel.position(startPosition);
            while (true) {
              channelBuffer.clear();
              long readPositionBefore = channel.position();
              int  readByteNum        = channel.read(channelBuffer);
              if (readByteNum == -1) {
//            读取结束
                break;
              }
              long readPositionAfter = channel.position();
              channelBuffer.flip();
              channelBuffer.get(channelBufferDest, 0, readByteNum);
              if (readPositionAfter > endPosition) {
//            超过当前段的结束位置需要调整本次读取的字节数
                readByteNum = new Long(endPosition - readPositionBefore).intValue();
              }
              bos.write(channelBufferDest, 0, new Long(readByteNum).intValue());
              if (readPositionAfter > endPosition) {
//            超过当前段的结束位置表示分割结束，同时调整channel的位置，为下一段的起始位置
                channel.position(endPosition);
                break;
              }
            }
          } catch (Exception e) {
            throw new FileOperationException("分割文件操作异常", e);
          }
        });
  }

  /**
   * 默认输出目录以 ["splits_" + 原文件名] 命名，在同个父级目录中。 每个分割文件的分段大小为50MB。 每个文件的名字以 [ "split_"+ 分段编号 + 原文件名] 命名。
   * 以系统默认行分隔符分隔。 处理分割时的缓存为1MB。查找分隔符的缓存为1KB
   *
   * @see #splitFileByMultiThread(File, File, long, FileSplitSegmentNamePolicy, byte[], int, int)
   */
  public static void splitFileByMultiThread(File sourceFile) throws IOException {

    AssertUtil.assertArgs(sourceFile.exists(), sourceFile.getName() + "不存在");
    //    创建splits+文件名的目录，存放分割后的文件
    String fileNameNoExtension = getFileNameNoExtension(sourceFile.getName());
    File outDir = sourceFile.toPath().getParent().resolve("splits_" + fileNameNoExtension)
                            .normalize().toFile();
    if (!outDir.exists()) {
      outDir.mkdirs();
    }
    splitFileByMultiThread(sourceFile, outDir, 50 * MB, new DefaultSplitFileNamePolicy(),
                           System.lineSeparator().getBytes(), MB, KB
    );
  }

  /**
   * 使用多线程将大文件分割
   *
   * @param sourceFile
   *     原大文件
   * @param outDir
   *     分割文件存放的目录
   * @param splitSize
   *     每个分割文件的大小，大致的一个大小，如果直接按这个大小分割，有可能会破坏文件内容。内部会对文件精确分割
   * @param splitFileNamePolicy
   *     分割文件命名的策略
   * @param separatorBytes
   *     分割文件内容的分隔符号的字符串的字节数组
   * @param splitFileCapacityBufferSize
   *     分割文件时的ByteBuffer缓存大小
   * @param prereadSeparatorBufferSize
   *     以分隔符精确查找分割位置时的ByteBuffer的缓存大小，不建议太小，也不建议太大了，使用时估算一下2-3倍一个内容的大小。 不然也会有少许问题，比如，正好在分隔符的位置破坏了
   */
  public static void splitFileByMultiThread(
      File sourceFile, File outDir, long splitSize,
      FileSplitSegmentNamePolicy splitFileNamePolicy, byte[] separatorBytes,
      int splitFileCapacityBufferSize, int prereadSeparatorBufferSize
  ) throws IOException {

    new FileSplitSegmentHandle(sourceFile, separatorBytes, splitSize, prereadSeparatorBufferSize)
        .split((sourceFilePath, segmentNum, startPosition, endPosition) -> {
          splitFileExecutorService
              .execute(new FileSegmentGenerator(sourceFilePath,
                                                new File(
                                                    outDir,
                                                    splitFileNamePolicy.transformer(
                                                        sourceFile.getName(), segmentNum)
                                                ).toPath()
                                                 .normalize().toString(), startPosition,
                                                endPosition,
                                                splitFileCapacityBufferSize
              ));
        });
  }

  /**
   * 可多线程将大文件按不同段用多任务处理，不必生成小文件。
   * todo 此方法只是简单演示输出为字符，有需要可以参考改变处理的逻辑即可
   */
  public static void segmentHandleFileByMultiThread(
      File sourceFile, long segmentSize,
      byte[] separatorBytes, int prereadSeparatorBufferSize
  ) throws IOException {

    new FileSplitSegmentHandle(sourceFile, separatorBytes, segmentSize, prereadSeparatorBufferSize)
        .split((sourceFilePath, segmentNum, startPosition, endPosition) -> {
          splitFileExecutorService
              .execute(new FileSegmentHandler(sourceFilePath, startPosition, endPosition,
                                              Charset.defaultCharset()
              ));
        });
  }

  public static int copy(InputStream in, OutputStream out) throws IOException {

    AssertUtil.assertNotNull(in, "No InputStream specified");
    AssertUtil.assertNotNull(out, "No OutputStream specified");

    try {
      return StreamUtil.copy(in, out);
    } finally {
      close(in);
      close(out);
    }
  }

  private static void close(Closeable closeable) {

    try {
      closeable.close();
    } catch (IOException ex) {
      // ignore
    }
  }

}
